Repository navigation
feat(client): format one of webpack's problems, wherever it is read - #2438
Conversation
🦋 Changeset detectedLatest commit: 57f595d The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
`client-src/overlay.js` was the last of it: an adapter whose substance was
turning webpack's error objects into the shape the shared overlay renders, and
a `send({ type })` state machine over `showProblems`/`clear`. Both belong
elsewhere. The formatting is webpack-dev-middleware's `client/problem` now,
and the state machine was standing in for per-source slots that overlay
already has, so the events map onto two calls.
What stays is this package's identity on top of a shared overlay, as options:
the `webpack-dev-server-client-overlay` element id, so anything querying it is
unaffected; the `webpack-dev-server#overlay` Trusted Types policy name, which
a page's CSP allowlists by name; and the `/webpack-dev-server/open-editor`
route that makes a file reference clickable.
`test/client/ReactErrorBoundary.test.js` goes with it — the heuristic it
covers lives in webpack-dev-middleware, which tests it.
Requires webpack/webpack-dev-middleware#2438.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA
The overlay took formatted strings only. The middleware formats its own
payloads on the server, so its client never needed anything else — but a
server that sends webpack's error objects to the browser and formats them
there had to write that formatting itself, which is what webpack-dev-server
carried a client file for.
`showProblems` now takes an error object as it comes, and the formatting is
`webpack-dev-middleware/client/problem`: `problemLocation`, `problemBody`,
`problemLine` and `formatProblem`, the last splitting a problem into the
header and body a console wants.
The server's own `formatErrors` builds the same shape, so a build reads the
same way whether the middleware formatted it or a server sent the object over.
Two things it got wrong fall out of that:
* An error webpack names no module for sent a first line holding a single
space. The overlay reads the first line as the heading, so it drew one
with nothing in it. The message goes alone now.
* A module built by loaders reports its whole request as `moduleName`
(`babel-loader!./app.js`), which reads as noise where the file is what
matters. The file comes first, the request follows it, and `file` is used
when webpack sets one — once, not appended to itself.
A unit test asserts the two formatters agree on every shape rather than
trusting them to.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA
1e47daa to
57f595d
Compare
|
Navigate logical layers of code changes, visualize relationships, and explore their blast radius. No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (10)
Included review availability: This review used your included allowance. Your plan provides up to 8 included reviews per hour; 7 remain after this review. WalkthroughThe change adds browser-side formatters for webpack problems and exposes them through the package and overlay declarations. The overlay now accepts strings or problem objects. Server error headings include available module, loader, file, and location details, and omit an empty heading when those details are absent. Tests cover client formatting, overlay rendering, and server output. Priority: ➖ Normal Merge Risk: ⚪ Minimal · up to The change adds structured problem formatting while preserving supported server inputs. No merge-blocking issue remains; merge after normal checks. Security Architecture ReviewSecurity architecture risk: 🔵 Low · up to The change primarily broadens diagnostic input and formatting. Structured problems are converted into strings before display, and no new privilege or boundary bypass was demonstrated. Risk remains low rather than minimal because HTML escaping and failure recovery could not be fully verified. Retained concerns Security review detailsSecurity Blast Radius
Trust Boundaries and Controls
Resilience and Maintainability Implications
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #2438 +/- ##
==========================================
+ Coverage 96.15% 96.22% +0.06%
==========================================
Files 19 20 +1
Lines 2161 2200 +39
==========================================
+ Hits 2078 2117 +39
Misses 83 83 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
`client-src/overlay.js` was the last of it: an adapter whose substance was
turning webpack's error objects into the shape the shared overlay renders, and
a `send({ type })` state machine over `showProblems`/`clear`. Both belong
elsewhere. The formatting is webpack-dev-middleware's `client/problem` now,
and the state machine was standing in for per-source slots that overlay
already has, so the events map onto two calls.
What stays is this package's identity on top of a shared overlay, as options:
the `webpack-dev-server-client-overlay` element id, so anything querying it is
unaffected; the `webpack-dev-server#overlay` Trusted Types policy name, which
a page's CSP allowlists by name; and the `/webpack-dev-server/open-editor`
route that makes a file reference clickable.
`test/client/ReactErrorBoundary.test.js` goes with it — the heuristic it
covers lives in webpack-dev-middleware, which tests it.
Requires webpack/webpack-dev-middleware#2438.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA
… here (#5750) * refactor(client): take the WebSocket transport from dev-middleware First step of moving the hot clients into webpack-dev-middleware: this package's `WebSocketClient` is a strict subset of the one that package now ships, and worse in three ways. It has no `close()` at all, despite declaring `@implements {CommunicationClient}`, which the interface requires. It has no guard against an event the socket had already queued reporting after the caller closed, so a close could schedule a reconnection nobody asked for. And it hands the url to `new WebSocket` unresolved, which throws on browsers before Chrome 125 / Firefox 124 / Safari 17.3 for a relative or `http(s):` url — this package always builds an absolute `ws:` url, so that one never bit here, but it is a trap for anyone reusing the class. Re-exported rather than deleted: `client.webSocketTransport` resolves to this path, so anything pointing at it keeps working. `import/no-unresolved` is switched off for the file, following the exemption already in place for `@changesets/get-github-info`: the import resolver cannot follow an `exports` subpath. TypeScript does resolve it, so `lint:types-client` still covers the import. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA * refactor(client): take the error overlay from dev-middleware too 692 lines of overlay deleted in favour of the one dev-middleware ships — the state machine, the iframe, the rendering, the runtime-error listeners. What is left is an adapter: this package's `createOverlay`/`send` shape on top of that overlay's `showProblems`/`clear`, plus `formatProblem`, which stays because the console uses it and because this package still receives webpack's error objects rather than formatted strings. Three things had to be carried across so nothing outward changes: * the element id. It is what a test, a screenshot tool or an integration finds the overlay by, so `webpack-dev-server-client-overlay` is passed through dev-middleware's new `overlay.id` rather than renamed; * the Trusted Types policy name. Under an enforced `require-trusted-types-for 'script'` the page's CSP allowlists a policy by name, and dev-middleware's default is a different one, so `webpack-dev-server#overlay` is passed explicitly — including where this package's option is `false`, which means "no name of my own"; * `/webpack-dev-server/open-editor`. dev-middleware leaves the endpoint empty by default, having no route to point at, so the file references in a problem would have stopped being clickable. The first two came from the e2e suite failing, not from reading the code, which is the argument for swapping under the tests rather than after. What does change is the overlay's internal DOM, which is not an interface this package documents: a clickable file reference is `[data-open-file]` rather than `[data-can-open]`, and a problem is headed by its level and origin rather than "Compiled with problems". Two tests assert on those and move with the implementation. Checked against a baseline taken first: the overlay suite fails exactly the set it fails on a clean `main` in this container — no case newly failing, none newly passing — and the client and web-socket-url suites are unmoved. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA * feat(client): add Server-Sent Events as a second transport `client.webSocketTransport: "sse"` connects the page with `EventSource` instead of a WebSocket. The endpoint is webpack-dev-middleware's hot endpoint, which is an ordinary response rather than an upgrade, so there is no second server to start and it sits in the middleware chain where everything this package puts in front of it can still see it. Naming it as the client's transport picks the endpoint that serves it, so `webSocketServer` does not have to be set as well; `webSocketServer: "sse"` is accepted too. `lib/servers/EventSourceServer.js` is the adapter between that endpoint and the transport shape the rest of this package is written against, so nothing above the wire can tell which one it is talking to — same protocol, same messages, same options. All of the protection stays here: the endpoint hands over the request each client connected with, and `allowedHosts` and the origin check decide as they do for a socket. One difference in how that check reads a request. A browser puts an `Origin` on a WebSocket handshake whether or not it is cross-origin, so one without it is refused; `EventSource` sends no `Origin` on a same-origin request, so for a stream an absent one is taken as a page of this server's own. A stream that does carry an `Origin` — which is what a cross-origin page sends — is checked as before, and the `Host` check runs either way. Both transports are the middleware's now, re-exported from `client-src/clients/`, and each has a test of its own: the contract both answer, plus the silence watchdog only a stream needs. `test/e2e/event-source.test.js` drives a real browser through a hot update over the stream, the handshake it is greeted with, and both sides of the host check. `socket.js` no longer throws on a frame it cannot parse. A stream's keep-alive has to be a `data:` frame rather than a comment, or the client's own watchdog would count a quiet connection as a dead one, so something that is not JSON now arrives on the wire by design. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA * refactor(client): no overlay code left to adapt `client-src/overlay.js` was the last of it: an adapter whose substance was turning webpack's error objects into the shape the shared overlay renders, and a `send({ type })` state machine over `showProblems`/`clear`. Both belong elsewhere. The formatting is webpack-dev-middleware's `client/problem` now, and the state machine was standing in for per-source slots that overlay already has, so the events map onto two calls. What stays is this package's identity on top of a shared overlay, as options: the `webpack-dev-server-client-overlay` element id, so anything querying it is unaffected; the `webpack-dev-server#overlay` Trusted Types policy name, which a page's CSP allowlists by name; and the `/webpack-dev-server/open-editor` route that makes a file reference clickable. `test/client/ReactErrorBoundary.test.js` goes with it — the heuristic it covers lives in webpack-dev-middleware, which tests it. Requires webpack/webpack-dev-middleware#2438. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA * feat: reuse webpack-dev-middleware's browser runtime This server has no client of its own any more. `client-src/` is gone — the runtime, the error overlay, the progress indicator, the logger, the socket wrapper and the WebSocket client, around two thousand lines that existed in near-duplicate in both projects — and the middleware's hot option drives all of it, including putting the entry and `HotModuleReplacementPlugin` into the compilation. Nothing about a configuration changes. Every option keeps its name and its meaning and is mapped onto the middleware's vocabulary in `lib/hotOptions.js`: `hot`/`liveReload` become one `apply` mode, `client.reconnect` becomes `connect.retries`, `client.webSocketURL` is already the shape the middleware's `hot.client.path` takes, and the overlay is passed through with this project's own element id so anything querying for it still finds it. The `webpack-dev-server/client/*` paths are generated re-exports of the middleware's client, so importing them resolves to what it always did. The paths that were only this bundle's internals are no longer published. Protection stays here. `allowedHosts`, the `Origin` checks and the same-origin rule are unchanged: the middleware is handed `cors: true` and no token, which is it applying no policy of its own, and every connection is judged here before anything is published to it. A handshake is refused before it completes rather than after, since the middleware exposes the upgrade rather than taking the server over. A `webSocketServer` of your own still works. A class, a module exporting one, or options giving the socket a port or a server of its own are wrapped into the shape the middleware asks a custom transport for, so there is one path from a build to a page either way, and `BaseServer` keeps its export. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA * refactor: serve "sse" from the middleware's own transport The adapter this branch added, `lib/servers/EventSourceServer.js`, existed to dress the middleware's hot endpoint up as the transport shape this server was written against. There is no such shape any more — the middleware owns both ends of both transports — so naming `sse` is now just choosing one of its transports, and the adapter and its special case in `normalizeOptions` are gone with it. `client.webSocketTransport: "sse"` and `webSocketServer: "sse"` keep working and keep picking each other, which is what the option values are for. The published `client/clients/EventSourceClient.js` path is generated alongside its WebSocket neighbour, so a transport named by module still resolves. One thing this caught: `"sse"` was being read as an implementation of this server's own, since the test for that was "not `ws`" — which sent it looking for a class to construct and failed before a page could connect. Both of the middleware's transports are named now, and a smoke run over each confirms the endpoint answers: `426` to a plain GET on the WebSocket one, an event stream on the other. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA * fix: keep this package's name and its refusals in the browser console Two things the browser used to say that the middleware's runtime had no way to say for it. The console prefix: every message read `[webpack-dev-server]`, and four snapshots record exactly that, including `Invalid Host/Origin header` — a line people search for. Taking the runtime as-is would relabel all of it with a dependency's name and send anyone with a problem to the wrong repository, so the name travels with the logging option. The refusal itself: a client turned away by `allowedHosts` or the origin check is told why, and the reason is logged in the page. That needs the handshake to complete, so the upgrade is answered even for a client that will be refused and `guardHotEndpoint` closes it immediately after saying so — which is what this server has always done. Owning the `upgrade` listener is still what keeps a proxied upgrade with whoever it belongs to; `isHotEndpoint` went with the earlier attempt to refuse before the handshake, which would have made the reason unreachable. Verified in a browser: the console carries `[webpack-dev-server]`, and a client from a disallowed origin receives `{"action":"error","message":"Invalid Host/Origin header"}` and is closed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA * test: wait for a build rather than the handshake's configuration push Two proxy cases waited for `{ type: "hot" }`, which was this server telling a client what to enable right after the handshake. That configuration is in the client's entry query now, so the first thing the socket says is the build the page is to run. What the cases are about is untouched and still asserted: the HMR upgrade must not reach the proxy target (`backendUpgradeCount` stays `0`), and must not make the proxy log an error. Both hold. `test/server/open-option.js` fails 33/33 here and on `main` alike — it drives a real browser through `open`, which this environment has none of. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA * fix: refuse an event-stream client, not just a WebSocket one `allowedHosts` and the origin check were not being enforced on the `sse` transport. The guard ran and decided to refuse, and then neither half of the refusal landed: the message went through `sendMessage`, which tests `readyState === 1` and calls `client.send` — a `ServerResponse` has neither, so the loop skipped it — and `client.close()`, which a `ServerResponse` also does not have, threw and was swallowed. The stream stayed open and kept receiving builds. The message goes through the middleware's `publishTo` now, which puts it on the wire the client is actually on, and the connection is dropped the way that client is dropped: a WebSocket closes, a response ends. Verified on both transports: a disallowed origin is told `Invalid Host/Origin header` and dropped, over a WebSocket and over a stream alike — and a same-origin page, which `EventSource` gives no `Origin` at all, is still let through. Found via a CodeRabbit review of the middleware's README example, which had the same two mistakes in it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA * fix: treat an empty `webSocketURL` part as one that was not given A `client.webSocketURL` written as a string is normalized by parsing it, which fills in every part — as `""` for the ones the url did not carry. Those were passed straight through to the middleware, which refuses an empty port: so `webSocketURL: "ws://example.test/ws"` stopped the server from starting at all. Empty has always meant "not said" here, and both the old client and the new one resolve such a part from the page, so it is left out rather than sent. The `allowed-hosts` snapshots are regenerated with it. Three changes, all consequences of the runtime moving rather than of anything going wrong: `connected` is logged where this server used to log which features were enabled, `[HMR] Waiting for update signal from WDS...` is gone with the `webpack/hot/dev-server` entry that printed it, and the prefix is still `[webpack-dev-server]`. The suite is 31 of 32 here; the one failure is an IPv6 case, and this container cannot listen on `::1` at all. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA * fix: drop a test of a file that no longer exists, and regenerate the types Two things CI's Lint job reports that are not about the dependency. `test/client/clients/EventSourceClient.test.js` was added earlier on this branch and imports `client-src/clients/EventSourceClient.js`, which is gone. It never survived the rebase as a deletion because it did not exist on the side that deleted the rest of `test/client/`. Every case in it tests the middleware's own stream client through a re-export, and the middleware's `test/client-transports.test.js` already covers all six — plus the ones only a stream has — so nothing is lost. `types/` is tracked and the Lint job fails with "Missing types" when a build changes it. `Server.d.ts` still described the removed methods, and the two new modules had no declarations at all. The changeset now says the two public methods that went with them, `getClientEntry()` and `getClientHotEntry()`, are a breaking removal. It recorded the rest of the removal and not that. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA * test: regenerate the web-socket-server-url console snapshots The suite reads the browser's console, which is now the middleware's client talking. Every difference is one the migration makes on purpose: - the startup banner, `Server started: Hot Module Replacement enabled, …`, and `[HMR] Waiting for update signal from WDS...` are gone — the old client printed the first from the configuration this server pushed after the handshake, and the second came from the `webpack/hot/dev-server` entry this server no longer adds; - `Hey.` comes first, because the page no longer waits on a banner; - the reconnect cases gain `Trying to reconnect...` and `Disconnected!`. Regenerated only after reading what kind of failure each of the 34 was: all snapshot mismatches, none of them an error. One test here was worse than a mismatch. "should not work and output disconnect wrong web socket URL" waits for `Disconnected!` with no timeout, and the middleware's client did not print it, so it spun forever with its server and browser open and took the whole file to the runner's 400 s limit. That is fixed in webpack-dev-middleware, which now says so when the connection goes away; with it the file runs in 53 s and passes twice running. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA * fix: keep manual client entries, page-url opt-outs and a hand-made middleware working - `client/index.js` is now CommonJS and leaves the query written on it where the middleware's client reads it, so `client/index.js?...` entries keep their options instead of connecting with defaults. - Name the client's page-url parameters after this package again, so `?webpack-dev-server-hot=false` and `?webpack-dev-server-live-reload=false` still opt a tab out. - `client: false` no longer throws while reading `client.webSocketTransport`. - A middleware mounted through `setupMiddlewares` has no hot endpoint of this server's, so there is nothing to guard. - Update the e2e expectations for the middleware's client: its messages, the order of `connected` after the page's own output, and a wait that no longer polls forever for a message the client does not log. - Drop the tests of the removed `getClientEntry()`/`getClientHotEntry()`. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA * test: bring the end-to-end suites in line with the middleware's client Every difference is one the migration makes on purpose, and the ones that were not are fixed: - Provide the transport through `__webpack_dev_server_client__` again for `ws` and `sse` too, as the global has always held the one in use, and have the `client/` re-exports hand back the default export of the modules that have one. - Point the overlay at this server's open-editor route. - Expect what the middleware's client logs, with the build's timing and absolute paths normalized, and its message for each way an update goes. - Port the tests that reached into the removed WebSocket server: the heartbeat, the client count, the host check on the event stream, and the federation entries, which are now checked on a browser target — a build for node is given no client. - Replace the tests of the old progress element with ones of what `client.progress` promises, the indicator's own being the middleware's. - Add unit tests for the options this server hands the middleware. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA * test: update the command line tests for the middleware's client, and say what differs The hot option is now visible in the client's entry rather than in a webpack/hot entry, and a build for node is given neither. The changeset lists what a page and its console look like now, which it had called nothing. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA * test: record what a CI browser sees, not what a sandbox does Snapshots regenerated in a sandbox carried its differences with them: - The overlay's style attributes came out with border: none, where the browser CI uses writes the four longhands the committed snapshots have. - A sandbox proxy refuses connections to the addresses the web socket URL tests use with a 403, so those entries recorded a refusal where CI connects or fails to resolve the name. - The IPv6 entry of the allowed hosts test could not run there, so its snapshot was dropped by the update. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA * test: write the changed file's path the same on every platform A rebuild's message names the file that changed, and a Windows machine writes that path with backslashes, so the snapshots that record it could only match on a machine that does not. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA * test: wait for the client to give up on a name that never resolves How long a machine takes to say a name does not resolve is its own, and the page was idle before the answer came on one runner, so the snapshot was taken of "Hey." alone. The test now waits for the message that ends the sequence it records. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA * test: wait for the worker's client to connect before the snapshot The worker's own client connects after the page has settled, so on a slow runner the snapshot could see one "connected" line instead of two. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA * chore: merge this change's changesets into one entry Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA * feat: leave the last of the compiler and socket wiring to the middleware dev-server no longer applies a ProvidePlugin or HotModuleReplacementPlugin, or runs a WebSocket server of its own: `client: false`, a custom `client.webSocketTransport`, `webSocketServer.options` (a port or server of its own, verifyClient, compression) and the HMR plugin for every compilation are webpack-dev-middleware's `hot` options now. lib/servers/WebsocketServer.js and bridge.js are gone; BaseServer stays as the contract for a `webSocketServer` implementation of your own. And what a page or a tool written against 6.0 relies on keeps working: - `client/socket.js` and this server's own `{ type }` messages, which React Refresh's overlay reads off the socket - `webSocketServer.clients` and `.implementation`, `sendMessage()` (deprecated; `static-changed` reloads), `getClientEntry()` and `getClientHotEntry()` (deprecated) - the Trusted Types policy `webpack-dev-server#overlay`, progress off unless asked, a one second ping, overlay filters handed `{ message }` - a hand-written `client/index.js` entry connects to `/ws` as before - a `setupMiddlewares` that replaces the dev middleware (a Hono app) keeps the hot endpoint - the editor is opened on the file resolved against webpack's context Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA * fix: keep a subclass's client entries, the client paths in use and universal builds working - `getClientEntry()` and `getClientHotEntry()` overridden by a subclass put their modules in `entry` again, the client one with the query this server always wrote, and the middleware then adds no client of its own. - `client/` keeps only the paths something outside this package uses: `index.js`, written into `entry` by hand; `socket.js`, which React Refresh reads; and `clients/WebSocketClient.js`, which a transport of someone else's extends, still with `default` for `require()`. - `client/index.js` hands its query over only where there is a `self`, so a universal build runs in Node without it throwing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA * test: main's suite on the middleware's client, and React Refresh's hot scenarios main's tests run as they are wherever they still pass; the rest only record what the middleware's client says and shows, in its console, overlay and progress indicator. A subclass's `getClientEntry()` and `getClientHotEntry()` keep main's test. React Refresh is tested with `@pmmmwh/react-refresh-webpack-plugin` itself: a component keeps its state across one edit and several, a child module updates in place, changed hooks remount in place, its overlay shows a build error and an error thrown while rendering and clears once fixed, an unaccepted edit reloads, `hot: "only"` does not, and all of it over Server-Sent Events too. A universal build is served to a page and run in Node. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA * chore(ci): TEMPORARY - install the unreleased webpack-dev-middleware DO NOT MERGE THIS COMMIT. Revert it, and set the dependency back to a released `^8.4.0`, once webpack-dev-middleware ships. This branch needs what the published 8.3.0 does not have: `hot.protocol` with this server's runtime in `client/webpack-dev-server/`, `onConnect` with the request, `handleUpgrade`, `publishTo`, `hot.ws` and the `ws` server handed out as `hot.implementation`. CI installs from the registry, so without this `tsc` fails in the Build step and every job behind it is cancelled. This vendors a pack of webpack-dev-middleware `main` plus webpack/webpack-dev-middleware#2472 (feat/finish-dev-server-migration). The lockfile change is confined to that one dependency. To undo, with the release out: git revert <this commit> npm install webpack-dev-middleware@^8.4.0 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA * refactor: follow the middleware's hot.client.url and pageParamPrefix names Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA * chore(ci): TEMPORARY - re-vendor webpack-dev-middleware with the settled option names Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA * test: check the hot option against webpack-dev-middleware's schema Builds the middleware from each shape of configuration, so a name renamed on either side fails here, and checks a hand-written client entry still honours this server's page-url parameters. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA * refactor: hand a custom client to hot.client.module, and turn off both halves together Follows webpack-dev-middleware's split: `client.webSocketTransport` naming a module now maps onto `hot.client.module`, with `transport` left as the wire; and "add nothing" is `client: false` plus `inject: false`, since `inject: false` alone now only leaves the plugin out. Re-vendors the middleware build carrying the change (TEMPORARY until it is released). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA * refactor: map client: false onto the middleware's hot.inject enum Reverts the `hot.client.module` mapping, so `client.webSocketTransport` goes to `hot.client.transport` again; `client: false` becomes `inject: "plugin"` with `hot` and `inject: "none"` without it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA * chore(ci): TEMPORARY - re-vendor webpack-dev-middleware with the hot.inject enum Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA * chore: mark sendStats and the client/socket shim for the next major release Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA * chore: regenerate types for the sendStats TODO Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA * chore(ci): TEMPORARY - re-vendor webpack-dev-middleware main (f084ec9) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA * chore(deps): depend on webpack-dev-middleware ^8.4.0 Replaces the TEMPORARY vendored build with the released package. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA * test: buffer split SSE frames and accept extra connects in the worker test Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Why
webpack-dev-server has no overlay code left after this, which is the point.
The overlay took formatted strings only. The middleware formats its own
payloads on the server, so its client never needed anything else — but a
server that sends webpack's error objects to the browser and formats them
there had to write that formatting itself. That is what webpack-dev-server's
client-src/overlay.jswas: 177 lines whose substance wasproblemLocation/problemBody/problemLine(webpack's error object → theoverlay's string shape) and
formatProblemfor its console. An adapter isstill a client file.
What
showProblemstakes an error object as it comes:and the formatting is a module of its own, for a console as much as an overlay:
problemLocation,problemBody,problemLineandformatProblem, exportedfrom
./client/problemand re-exported from./client/overlayso a consumerdoing both has one import.
Two things the server got wrong
src/hot.jsbuilds the same shape now, so a build reads the same way whetherthe middleware formatted it or a server sent the object over. A unit test
asserts the two agree across every shape rather than trusting them to.
Falling out of that:
" \nmessage"— a first line holding a single space — and the overlay readsthe first line as the heading. The existing test pinned it
(
expect(formatErrors([{ message: "boom" }])).toEqual([" \nboom"])). It isreachable: a real build's
Module not found: Error: Can't resolve 'x'for anentry comes through as
{ loc: "c", message: … }with nomoduleName. Themessage now goes alone.
its whole request as
moduleName, which reads as noise where the file iswhat matters. The file comes first now, the request after it, and
fileisused when webpack sets one — once, not appended to itself
(
./a.js (./a.js)), which is what both the naive version and my own firstattempt produced.
test/problem.test.jscovers that case because it caughtme.
I checked what webpack 5 actually sets before writing these rules, rather than
assuming: for a parse error, a missing module, and a loader failure,
moduleNameis the bare module (./src/c.js) andfileis unset — so theloader and
filebranches are rules for input this project does not produceitself, and matter for a server that sends its own objects.
Proof it does the job
On a dev-server checkout with this packed and installed,
client-src/overlay.jsandtest/client/ReactErrorBoundary.test.js(the Reacterror-boundary heuristic is tested here already) delete outright, and
client-src/index.jscalls this package directly:with the id, the Trusted Types policy name and the open-in-editor route as
configureOverlayoptions,showProblems/clearOverlayin place of thesend({ type })state machine — the per-source slots are what that machinewas for — and
formatProblemfor the console. dev-server's overlay e2e suite:39 pass. That change goes on webpack/webpack-dev-server#5750.
showProblems("warnings", filtered, source)is also called there without alength check, which #2437 makes safe.
State
e2e 138 pass across 12 suites, unit 6909 across 17 (
test/problem.test.jsis18 of them,
formatErrorsgained 5), lint / both typecheck passes / spelling /the precompiled schema check / the full build clean.
test/logging.test.jshasthe two root-only
chmodfailures it has onmainin this container.🤖 Generated with Claude Code
https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA
Generated by Claude Code
Summary by CodeRabbit